# Java 8 中的新特性主要有哪些?

  • Lambda表达式
  • 接口的默认方法与静态方法
  • 方法引用
  • 重复注解
  • Optional
  • Stream API, 并行流
  • Date/Time API (JSR 310)

# 什么是 Java 8 中的异步编程?为什么要使用 CompletableFuture?Future和CompletableFuture的区别是什么?

Future在 Java5 就引入了。

优点:一定程度上让一个线程池内的任务异步执行了

缺点:传统回调最大的问题就是不能将控制流分离到不同的事件处理器中。例如主线程等待各个异步执行的线程返回的结果来做下一步操作,则必须阻塞在future.get()的地方等待结果返回。这时候又变成同步了。

CompletableFuture在Java8引入。默认使用 ForkJoinPool.commonPool() 作为它的线程池执行异步代码。

实现了Future和CompletionStage接口,保留了 Future 的优点,并且弥补了其不足。即异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose 等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。

可见,这种方式才是我们需要的异步处理。一个控制流的多个异步事件处理能无缝的连接在一起。

# synchronized

# 什么会需要synchronized?什么场景下使用synchronized?

synchronized关键字用于解决多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

# synchronized 关键字的三种使用方法

  • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁,相当于 synchronized(this)。
  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得 当前 class 的锁,相当于 synchronized(类.class)。
  • 修饰代码块:给括号内配置的对象加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁。

# 为什么 JDK1.6 之前,synchronized 效率低下?

JDK1.6之前,synchronized属于重量级锁,JVM 在实现 synchronized 时,使用的监视器锁【monitor】依赖于底层操作系统的 Mutex Lock 实现, Java 线程模式和操作系统的原生线程为一对一,也就是一个 Java 线程实际上对应一个操作系统的原生线程。如果要挂起或唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转化到内核态,需要消耗比较长的时间。

但是,JDK1.6之后,Java官方从JVM层面对synchronized关键字进行了较大的优化,效率不可同日而语。主要的优化有:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

# JDK1.6之后对 synchronized 关键字进行了哪些优化?

锁主要存在的四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

锁升级的过程:无锁,偏向锁,轻量级锁,重量级锁 image

# 偏向锁,轻量级锁,重量级锁 的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

# 总结

  1. JVM在JDK 1.6中引入了分级锁机制来优化synchronized
  2. 当一个线程获取锁时,首先对象锁成为一个偏向锁
    • 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换
  3. 如果有多个线程竞争锁资源,锁将会升级为轻量级锁
    • 这适用于在短时间内持有锁,且分锁交替切换的场景
    • 轻量级锁还结合了自旋锁来避免线程用户态与内核态的频繁切换
  4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
  5. 优化 synchronized 同步锁的关键:减少锁竞争
    • 应该尽量使 synchronized 同步锁处于轻量级锁或偏向锁,这样才能提高 synchronized 同步锁的性能
    • 常用手段
      • 减少锁粒度:降低锁竞争
      • 减少锁的持有时间,提高 synchronized 同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁
  6. 在锁竞争激烈时,可以考虑禁用偏向锁和禁用自旋锁

# ReentrantLock

ReentrantLock 是作用类似于 synchronized 的一个同步器,由于 synchronized 性能低下,因此 Java 设计了 ReentrantLock 类。

# ReentrantLock 加锁、解锁流程(AQS 原理)

  1. 加锁
    • 当调用 ReentrantLock.lock(); 方法时,开始尝试加锁。
    • 如果当前锁没有被其他线程加锁,即锁状态 state == 0 时,通过 compareAndSetState(0, 1) 更新锁状态, 再把 ExclusiveOwnerThread 设置为当前线程,加锁成功。
    • 如果当前锁已经被其他线程加锁,即锁状态 state > 0 时,通过 compareAndSetState(0, 1) 更新锁状态就不会成功,此时进入 acquire(1); 方法,此时,
      • 非公平锁:先尝试 cas 获取锁,如果 state = 0, 则加锁成功,否则判断是否为重入锁,如果是重入锁,则加锁成功。否则加锁失败,进入队列。
      • 公平锁:先看前面有没有排队的线程,如果有先去排队,然后再执行上面 “非公平锁” 的步骤。
    • 注意:在创建 ReentrantLock 对象时,可以使用有参的构造函数指定是公平锁还是非公平锁,默认为非公平锁,效率高点儿,因为不用排队。
  2. 解锁
    • 当调用 ReentrantLock.unlock(); 方法时,开始解锁。
    • 首先 getState() - 1,再把 ExclusiveOwnerThread 设置为 null, 最后设置 state = getState() - 1,完成解锁。
    • 注意:由上面的 getState() - 1 可以看出,如果加了两次锁,即 ReentrantLock.lock() 执行了两次,那么也需要 ReentrantLock.unlock(); 执行两次才能解除所有锁。

# 为什么 ReentrantLock.lock() 不能被 t1.interrupt(); 打断,而 ReentrantLock.lockInterruptibly(); 可以被打断?

主要差异如下:

// ReentrantLock.lock(); 最后会执行这个方法
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 在这里,即使执行了 t1.interrupt();,也只是更改了局部变量 interrupted 的状态,整个代码依然在 for (;;) 的死循环里,并没有退出,所以,无法打断。
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// ReentrantLock.lockInterruptibly(); 最后会执行这个方法
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 在这里,执行了 t1.interrupt();,抛出了一个异常,整个代码跳出 for (;;) 的死循环,最后取消排队,然后打断线程。
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# synchronized 关键字与 ReentrantLock 的区别?

  1. 共同点 都是可重入锁:自己可以再次获取自己的内部锁【避免一个线程获取锁之后,再次尝试获取锁时造成的死锁】。同一线程每次获取锁,计数器加一,释放锁,计数器减一,计数为0,代表完全释放该锁。

  2. 不同点

    • synchronized依赖于JVM实现,ReentrantLock依赖于API。
    • 相比synchronized,ReentrantLock 增加了一些高级功能。主要来说主要有三点:
      • 等待可中断: ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
      • 可实现公平锁: ReentrantLock 可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
      • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

# 优先使用 synchronized 还是 ReentrantLock?

  1. 在高并发场景下,为了提高性能,就要避免 synchronized 升级为重量锁(java 6 以后),而 synchronized 锁膨胀不可逆,意味着锁不会降级,一旦最后升级为重量级锁,那么会大大影响并发量,确实不适合用在高并发场景,此时更适合使用 AQS(CAS) 无锁,即 ReentrantLock。而 Java 6 之前,synchronized 为重量锁,所以此时也优先使用 ReentrantLock。
  2. 在非高并发场景下(有并发,但并发量不算高),可以从以下方面选择:
    • 性能。由于 synchronized 在 Java 8 之后做了优化,二者在这种情况下,性能几乎没有区别,而使用 synchronized 代码更简单,可以考虑优先使用。
    • 功能。如果有以下功能上的需求,只能使用 ReentrantLock, 否则使用 synchronized 更简单。
      • lock 可中断。包括人为中断和超时中断。
      • 可以多条件等待和唤醒。使用 ReentrantLock 中的 Condition。
      • 要指定使用公平锁或者非公平锁。而 synchronized 只能是非公平锁。
      • 支持读写锁。

# AQS

参考博客: 并发编程之 AQS(AbstractQueuedSynchronizer) 框架

# 并发下的 Map 常见面试题汇总

# HashMap 和 HashTable 有什么区别?

  • HashMap 是线程不安全的,HashTable 是线程安全的;
  • 由于线程安全,所以 HashTable 的效率比不上 HashMap;
  • HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null, 而 HashTable 不允许;
  • HashMap 默认初始化数组的大小为 16,HashTable 为 11,前者扩容时, 扩大两倍,后者扩大两倍+1;
  • HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

# Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?

ConcurrentHashMap 类(是 Java 并发包 java.util.concurrent 中提供的一 个线程安全且高效的 HashMap 实现)。

HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁); 而针对 ConcurrentHashMap,在 JDK 1.7 中采用分段锁的方式;JDK 1.8 中 直接采用了 CAS(无锁算法)+ synchronized,也采用分段锁的方式并大大缩小了锁的粒度。

# HashMap & ConcurrentHashMap 的区别?

  • 除了加锁,原理上无太大区别。
  • 另外,HashMap 的键值对允许有 null,但是 ConCurrentHashMap 都不允许。
  • 在数据结构上,红黑树相关的节点类

# 为什么 ConcurrentHashMap 比 HashTable 效率要高?

HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;

ConcurrentHashMap JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一 个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。

JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结 点)(实现 Map.Entry<K,V>)。锁粒度降低了。

# 针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?

JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。

  • Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;
  • HashEntry 用来封装映射表的键-值对;
  • 每个桶是由若干个 HashEntry 对象链接起来的链表。

JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超 过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。

# ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?

  1. JVM 开发团队在 1.8 中对 synchronized 做了大量性能上的优化,而且基 于 JVM 的 synchronized 优化空间更大,更加自然。
  2. 在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

# ConcurrentHashMap 简单介绍?

  • 重要的常量: private transient volatile int sizeCtl; 当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容; 当为 0 时,表示 table 还没有初始化; 当为其他正数时,表示初始化或者下一次进行扩容的大小。
  • 数据结构: Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据; TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据; TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。
  • 存储对象时(put() 方法):
    1. 如果没有初始化,就调用 initTable() 方法来进行初始化;
    2. 如果没有 hash 冲突就直接 CAS 无锁插入;
    3. 如果需要扩容,就先进行扩容;
    4. 如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形 式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
    5. 如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循环
    6. 如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。
  • 扩容方法 transfer():默认容量为 16,扩容时,容量变为原来的两倍。 helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。
  • 获取对象时(get()方法):
    1. 计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;
    2. 如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find()方法,查找该结点,匹配就返回;
    3. 以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。

# ConcurrentHashMap 的并发度是什么?

1.7 中程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。当用户设置并发度时, ConcurrentHashMap 会使用大于等于该值的最小 2 幂指数作为实际并发度(假如 用户设置并发度为 17,实际并发度则为 32)。

1.8 中并发度则无太大的实际意义了,主要用处就是当设置的初始容量小于并发度,将初始容量提升至并发度大小。

# 在 java 中守护线程和用户线程的区别?

java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。 任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(bool on);true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在 Thread.start()之前调用,否则运行时会抛出异常。

两者的区别: 唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经结束,Daemon 没有可服务的线程,JVM 关闭。

扩展:Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程

# 线程与进程的区别

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。 一个程序至少有一个进程, 一个进程至少有一个线程。

# 什么是多线程中的上下文切换

多线程会共同使用一组计算机上的 CPU,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU。不同的线程切换使用 CPU 发生的切换数据等就是上下文切换。

# 死锁与活锁的区别,死锁与饥饿的区别?

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件: 进程已获得资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。

活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”。而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

# synchronized 底层实现原理

synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来实现,相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。

JVM 就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。 注意,这个问题可能会接着追问,java 对象头信息,偏向锁,轻量锁,重量级锁及其他们相互间转化。

# 什么是线程组,为什么在 Java 中不推荐使用?

ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

  1. 线程组 ThreadGroup 对象中比较有用的方法是 stop、resume、suspend 等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。
  2. 线程组 ThreadGroup 不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。

# 什么是 Executors 框架?为什么使用 Executor 框架?

Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。 调用 new Thread()创建的线程缺乏管理,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。 接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、 定时定期执行、线程中断等都不便实现。

# 在 Java 中 Executor 和 Executors 的区别?

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。 Executor 接口对象能执行我们的线程任务。 ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。 使用 ThreadPoolExecutor 可以创建自定义线程池。

# 什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。 处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作—— Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。

java.util.concurrent.atomic 下提供了大量的原子操作类,比如原子类: AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference ,原子数组: AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray ,原子属性更新器: AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

# Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比 synchronized 它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。 他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:可以使锁更公平,可以使线程在等待锁的时候响应中断,可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,可以在不同的范围,以不同的顺序获取和释放锁。

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

# 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现 生产者-消费者模型?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列。在实现上,主要是利用了 Condition 和 Lock 的等待通知模式。

# 什么是 Callable 和 Future?

Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。

可以认为是带有回调的 Runnable。

Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果。

# 什么是 FutureTask?

在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给 Executor 来执行。

# 什么是并发容器的实现?

何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如 果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable, 以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。

# 多线程同步和互斥有几种实现方法,都是什么?

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。

# 什么是竞争条件?

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)。

# 为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?

当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。 但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run 方法当作普通方法去执行。

# 在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?

CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。

# 什么是不可变对象,它对写并发应用有什么帮助?

不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据, 也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。

不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。 既然它们的状态无法修改,这些常量永远不会变。

  • 不可变对象永远是线程安全的。
  • 只有满足如下状态,一个对象才是不可变的;
  • 它的状态不能在创建后再被修改;
  • 所有域都是 final 类型;并且,它被正确创建

# notify()和 notifyAll()有什么区别?

当一个线程进入 wait 之后,就必须等其他线程 notify/notifyall, 使用 notifyall, 可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。

如果没把握,建议 notifyAll,防止 notigy 因为信号丢失而造成程序异常。

# 什么是可重入锁(ReentrantLock)?谈谈它的实现。

线程可以重复进入任何一个它已经拥有的锁所同步着的代码块, synchronized、ReentrantLock 都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每释放一次锁, 进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。

# 当一个线程进入某个对象的一个 synchronized 的实例方法后,其它线程是否可进入此对象的其它方法?

如果其他方法没有 synchronized 的话,其他线程是可以进入的。 所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。

# 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。 Java 里面的同步原语 synchronized 关键字的实现是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。在 Java 中原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式:

  • 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
  • java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

# 什么是 CAS 操作,缺点是什么?

CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。每一个 CAS 操作过程都包含三个运算符:一个内存地址 V,一个期望的值 A 和一个新值 B,操作的时候如果这 个地址上存放的值等于这个期望的值 A,则将地址上的值赋为新值 B,否则不做 任何操作。

CAS 缺点:

ABA 问题: 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内 存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变 成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。 尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。

从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从 而浪费更多的 CPU 资源,效率低于 synchronized。

只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

# SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。 ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

# 写时复制容器可以用于什么应用场景?

CopyOnWrite 并发容器用于对于绝大部分访问都是读,且只是偶尔写的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。

透露的思想

  • 读写分离,读和写分开
  • 最终一致性
  • 使用另外开辟空间的思路,来解决并发冲突

# volatile 有什么用?能否用一句话说明下 volatile 的应用场景?

volatile 保证内存可见性和禁止指令重排。 volatile 用于多线程环境下的一写多读,或者无关联的多写。

# 为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果;
  • 存在数据依赖关系的不允许重排序

# 在 java 中 wait 和 sleep 方法的不同?

最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。

# 一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。hread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。

# 为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面?

JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的 wait()方法就有意义了。如果 wait()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。

# 什么是 ThreadLocal 变量?

ThreadLocal 是 Java 里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。

# Java 中 interrupted 和 isInterrupted 方法的区别?

interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java 多线程的中断机制是用内部标识来实现的,调用 Thread.interrupt()来 中断一个线程就会设置中断标识为 true。当中断线程调用静态方法 Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法 isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。

# 为什么 wait 和 notify 方法要在同步块中调用?

主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。

# 为什么你应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在 notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait()方法效果更好的原因

# 怎么检测一个线程是否拥有锁?

在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。

# 你如何在 Java 中获取线程堆栈?

kill -3 [java pid] 不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,

kill -3 tomcat pid, 输出堆栈到 log 目录下。

Jstack [java pid] 这个比较简单,在当前终端显示,也可以重定向到指定文件中。 或者使用 Java 提供的拟机线程系统的管理接口 ManagementFactory.getThreadMXBean()。

# Java 线程池中 submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定 义在 Executor 接口中。 而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口

# 你对线程优先级的理解是什么?

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。 我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。 java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。

# 你如何确保 main()方法所在的线程是 Java 程序最后结束的线程?

可以使用 Thread 类的 join()方法(或者 CountDownLatch 工具类)来确保所有程序创建的线程在 main()方法退出前结束。

# 为什么 Thread 类的 sleep()和 yield ()方法是静态的?

Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

# 现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

可以用 join 方法实现。

# 你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

volatile 关键字,读写锁,写时复制等等都可以实现。

# 用 Java 实现阻塞队列?

# 用 Java 写代码来解决生产者——消费者问题?

阻塞队列实现即可,也可以用 wait 和 notify 来解决这个问题,或者用 Semaphore

# 用 Java 编程一个会导致死锁的程序,你将怎么解决?

# Java 中如何停止一个线程?

使用共享变量的方式。在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。

使用 interrupt 方法终止线程。如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢? 比如当一个线程由于需要等候键盘输入而被阻塞,或者调用 Thread.join()方法, 或者 Thread.sleep()方法,在网络中调用 ServerSocket.accept()方法,或者调用了 DatagramSocket.receive()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为 true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。所以应该尽量使用 Thread 提供的 interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。

# JVM 中哪个参数是用来控制线程的栈堆栈大小的

-Xss

# 如果同步块内的线程抛出异常锁会释放吗?

# 单例模式的双重检查实现是什么?为什么并不安全?如何在 Java 中创建线 程安全的 Singleton?

不安全的根本原因是重排序会导致。未初始化完成的对象可以被其他线程看见而导致错误。创建安全的单例模式有:延迟占位模式、在声明的时候就 new 这个类的实例、枚举

# 写出 3 条你遵循的多线程最佳实践

  • 给你的线程起个有意义的名字。 这样可以方便找 bug 或追踪。 OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至 JDK 都遵循这个最佳实践。
  • 避免锁定和缩小同步的范围。锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
  • 多用同步类少用 wait 和 notify。 首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
  • 多用并发集合少用同步集合。这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。比如并发编程的黄金原则,尽量无锁化编程等等……..

# 请概述线程池的创建参数,怎么样合理配置一个线程池的参数?

参见笔记中线程池一章的内容

# 请概述锁的公平和非公平,JDK 内部是如何实现的。

公平锁是指所有试图获得锁的线程按照获取锁的顺序依次获得锁,而非公平锁则是当前的锁状态没有被占用时, 当前线程可以直接占用, 而不需要等待。在实现上,非公平锁逻辑基本跟公平锁一致,唯一的区别是,当前线程不需要判断同步队列中是否有等待线程。

非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用 cpu 的时间片, 尽量的减少 cpu 空闲的状态时间。

使用场景的话呢,其实还是和他们的属性一一相关,比如:如果业务中线程占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁可以保证不会有线程被饿死。

# 请概述 AQS

是用来构建锁或者其他同步组件的基础框架,比如 ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 就是基于 AQS 实现的。它使用了一 个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。它是 CLH 队列锁的一种变体实现。它可以实现 2 种同步方式:独占式,共享式。

AQS 的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如 tryAcquire、tryReleaseShared 等等。

这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程 的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。

在内部,AQS 维护一个共享资源 state,通过内置的 FIFO 来完成获取资源线程的排队工作。该队列由一个一个的 Node 结点组成,每个 Node 结点维护一个 prev 引用和 next 引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。同时与 Condition 相关的等待队列,节点类型也是 Node,构成一个单向链表。

# 请概述 volatile

volatile 关键字的作用主要有两点: 多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据。但是 volatile 不能保证操作的原子,对任意单个 volatile 变量的读/写具有原子性,但类似于++这种复合操作不具有原子性。

代码底层在执行时为了获取更好的性能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止重排序,当然这也一定程度上降低了代码执行效率。

同时在内存语义上,当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存,当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

在 Java 中对于 volatile 修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序问题、强制刷新和读取。

在具体实现上,volatile 关键字修饰的变量会存在一个“lock:”的前缀。它 不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock 会对 CPU 总线和高速缓存加锁,可以理解为 CPU 指令级的一种锁。

同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他 CPU 里缓存了该地址的数据无效。